package com.google.srclib.io.zip;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.h2.constant.SysProperties;
import org.h2.engine.Constants;
import org.h2.message.Message;
import org.h2.util.JdbcUtils;
import org.h2.util.New;

import com.google.srclib.dataset.SimpleResultSet;


/**
 * 
 * @author YLiang
 * 
 */
public class Zipper {
	private static Zipper singleton = null;

	private final static String fieldSeparatorWrite = ",";
	private char fieldSeparatorRead = ',';
	private char commentLineStart = '#';
	private static String rowSeparatorWrite;
	private static char fieldDelimiter = '\"';
	private static char escapeCharacter = '\"';
	private static String lineSeparator = SysProperties.LINE_SEPARATOR;
	private static String nullString = "";
	private String[] columnNames;
	private int inputBufferPos;
	private int inputBufferStart = -1;
	private int inputBufferEnd;
	private byte[] inputBuffer;
	private boolean endOfLine, endOfFile;

	public static Zipper getInstance() {
		if (singleton == null) {
			singleton = new Zipper();
		}
		return singleton;
	}

	/**
	 * 
	 * @param zipfilename
	 *            ZIP file name
	 * @return ZipOutputStream
	 */
	public ZipOutputStream makeZipFile(String zipfilename) {
		ZipOutputStream outFile = null;
		try {
			outFile = new ZipOutputStream((OutputStream) new FileOutputStream(
					zipfilename));
			outFile.setMethod(ZipOutputStream.DEFLATED);
		} catch (FileNotFoundException e) {
			System.out.println(Messages.getString("Zipper.WriteError")); //$NON-NLS-1$
		}
		return outFile;
	}

	/**
	 * 
	 * @param zout
	 *            ZipOutputStream
	 * @param rs
	 *            SimpleResultSet
	 * @param entryname
	 *            entry name
	 * 
	 * @throws SQLException
	 * @throws IOException
	 */
	public void csvWriter(ZipOutputStream zout, SimpleResultSet rs,
			String entryname) throws SQLException, IOException {
		ZipEntry entry = new ZipEntry(entryname);
		zout.putNextEntry(entry);
		write(zout, rs);
		zout.closeEntry();
	}

	private int write(ZipOutputStream zipout, ResultSet rs) throws SQLException {

		try {
			ResultSetMetaData meta = rs.getMetaData();
			int rows = 0;
			int columnCount = meta.getColumnCount();
			String[] row = new String[columnCount];
			for (int i = 0; i < columnCount; i++) {
				row[i] = meta.getColumnLabel(i + 1);
			}
			writeRow(row, zipout);
			rs.beforeFirst();
			while (rs.next()) {
				for (int i = 0; i < columnCount; i++) {
					row[i] = rs.getString(i + 1);
				}
				writeRow(row, zipout);
				rows++;
			}
			return rows;
		} catch (IOException e) {
			throw Message.convertIOException(e, null);
		} finally {
			JdbcUtils.closeSilently(rs);
		}
	}

	private void writeRow(String[] values, ZipOutputStream zipout)
			throws IOException {
		byte[] buf = new byte[1024];
		for (int i = 0; i < values.length; i++) {
			if (i > 0) {
				if (fieldSeparatorWrite != null) {
					buf = fieldSeparatorWrite.getBytes();
					zipout.write(buf, 0, buf.length);
				}
			}
			String s = values[i];
			if (s != null) {
				if (escapeCharacter != 0) {
					if (fieldDelimiter != 0) {
						zipout.write(fieldDelimiter);
					}
					zipout.write(escape(s).getBytes());
					if (fieldDelimiter != 0) {
						zipout.write(fieldDelimiter);
					}
				} else {
					buf = s.getBytes();
					zipout.write(buf, 0, buf.length);
				}
			} else if (nullString != null && nullString.length() > 0) {
				buf = nullString.getBytes();
				zipout.write(buf, 0, buf.length);
			}
		}
		if (rowSeparatorWrite != null) {
			buf = rowSeparatorWrite.getBytes();
			zipout.write(buf, 0, buf.length);
		}
		buf = lineSeparator.getBytes();
		zipout.write(buf, 0, buf.length);
	}

	private String escape(String data) {
		if (data.indexOf(fieldDelimiter) < 0) {
			if (escapeCharacter == fieldDelimiter
					|| data.indexOf(escapeCharacter) < 0) {
				return data;
			}
		}
		StringBuilder buff = new StringBuilder(data.length());
		for (int i = 0; i < data.length(); i++) {
			char ch = data.charAt(i);
			if (ch == fieldDelimiter || ch == escapeCharacter) {
				buff.append(escapeCharacter);
			}
			buff.append(ch);
		}
		return buff.toString();
	}

	/**
	 * 
	 * @param zipfilename
	 *            ZIP file name
	 * 
	 * @return ZipInputStream
	 */
	public ZipInputStream getZipFile(String zipfilename) {
		ZipInputStream inFile = null;
		try {
			inFile = new ZipInputStream(new FileInputStream(zipfilename));
		} catch (FileNotFoundException e) {
			System.out.println(Messages.getString("Zipper.OpenError")); //$NON-NLS-1$
		}
		return inFile;
	}

	public FileOutputStream extractFromZip(ZipInputStream infile,
			String filename) {
		File file = null;
		byte[] buf = new byte[1024];
		long filesize = 0;
		try {
			ZipEntry entry = null;
			while ((entry = infile.getNextEntry()) != null) {
				if (filename.equalsIgnoreCase(entry.getName())) {
					file = new File(filename);
					FileOutputStream fos = new FileOutputStream(file);
					int length = 0;
					while ((length = infile.read(buf, 0, buf.length)) != -1) {
						filesize += length;
						fos.write(buf, 0, length);
					}
					return fos;
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	public String changeExtension(String originalName, String newExtension) {
		int lastDot = originalName.lastIndexOf("."); //$NON-NLS-1$
		if (lastDot != -1) {
			return originalName.substring(0, lastDot) + newExtension;
		} else {
			return originalName + newExtension;
		}
	}

	/**
	 * 
	 * @param file
	 *            input zip stream to close
	 */
	public void closeZip(ZipInputStream file) {
		try {
			file.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void appendToZip(FileInputStream fis, String filename,
			ZipOutputStream outFile) {
		try {
			byte[] buf = new byte[1024];
			long filesize = 0;
			CRC32 crc = new CRC32();
			crc.reset();
			ZipEntry entry = new ZipEntry(filename);
			outFile.setLevel(6);
			try {
				outFile.putNextEntry(entry);
				int length = 0;
				while ((length = fis.read(buf, 0, buf.length)) != -1) {
					filesize += length;
					outFile.write(buf, 0, length);
				}
				entry.setSize(filesize);
				crc.update(buf);
				entry.setCrc(crc.getValue());
				outFile.closeEntry();
			} catch (ZipException e) {
				System.out.println(Messages.getString("Zipper.AppendZipError")); //$NON-NLS-1$
			}
			fis.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 
	 * @param zin
	 *            ZipInputStream
	 * @param entryname
	 *            entryname
	 * @return ResultSet
	 * 
	 * @throws IOException
	 * @throws SQLException
	 */
	public ResultSet csvReader(ZipInputStream zin, String entryname)
			throws IOException, SQLException {
		reset();
		ZipEntry entry = null;
		while ((entry = zin.getNextEntry()) != null) {
			if (entryname.equalsIgnoreCase(entry.getName())) {
				ResultSet rs = readResultSet(zin, null);
				zin.closeEntry();
				return rs;
			}
		}
		return null;
	}

	/**
	 * @param in
	 *            InputStream
	 * @return ResultSet
	 * 
	 * @throws SQLException
	 * @throws IOException
	 */
	public ResultSet csvReader(InputStream in) throws SQLException, IOException {
		reset();
		ResultSet rs = readResultSet(in, null);
		return rs;
	}

	private ResultSet readResultSet(InputStream zin, String[] colNames)
			throws SQLException, IOException {
		this.columnNames = colNames;
		initRead(zin);
		SimpleResultSet result = new SimpleResultSet();
		makeColumnNamesUnique();
		for (String columnName : columnNames) {
			result.addColumn(columnName, Types.VARCHAR, Integer.MAX_VALUE, 0);
		}
		Object[] row;
		while ((row = readRow(zin)) != null)
			result.addRow(row);
		return result;
	}

	private void makeColumnNamesUnique() {
		for (int i = 0; i < columnNames.length; i++) {
			String x = columnNames[i];
			if (x == null || x.length() == 0) {
				x = "C" + (i + 1);
			}
			for (int j = 0; j < i; j++) {
				String y = columnNames[j];
				if (x.equals(y)) {
					x += "1";
					j = -1;
				}
			}
			columnNames[i] = x;
		}
	}

	private void initRead(InputStream zin) throws IOException {
		inputBuffer = new byte[Constants.IO_BUFFER_SIZE * 2];
		if (columnNames == null) {
			readHeader(zin);
		}

	}

	private void readHeader(InputStream zin) throws IOException {
		ArrayList<String> list = New.arrayList();
		while (true) {
			String v = readValue(zin);
			if (v == null) {
				if (endOfLine) {
					if (endOfFile || list.size() > 0) {
						break;
					}
				} else {
					v = "COLUMN" + list.size();
					list.add(v);
				}
			} else {
				if (v.length() == 0) {
					v = "COLUMN" + list.size();
				} else if (isSimpleColumnName(v)) {
					v = v.toUpperCase();
				}
				list.add(v);
				if (endOfLine) {
					break;
				}
			}
		}
		columnNames = new String[list.size()];
		list.toArray(columnNames);
	}

	private String readValue(InputStream zin) throws IOException {
		endOfLine = false;
		inputBufferStart = inputBufferPos;
		while (true) {
			int ch = readChar(zin);
			if (ch == fieldDelimiter) {
				// delimited value
				boolean containsEscape = false;
				inputBufferStart = inputBufferPos;
				int sep;
				while (true) {
					ch = readChar(zin);
					if (ch == fieldDelimiter) {
						ch = readChar(zin);
						if (ch != fieldDelimiter) {
							sep = 2;
							break;
						}
						containsEscape = true;
					} else if (ch == escapeCharacter) {
						ch = readChar(zin);
						if (ch < 0) {
							sep = 1;
							break;
						}
						containsEscape = true;
					} else if (ch < 0) {
						sep = 1;
						break;
					}
				}
				String s = new String(inputBuffer, inputBufferStart,
						inputBufferPos - inputBufferStart - sep);
				if (containsEscape) {
					s = unEscape(s);
				}
				inputBufferStart = -1;
				while (true) {
					if (ch == fieldSeparatorRead) {
						break;
					} else if (ch == '\n' || ch < 0 || ch == '\r') {
						endOfLine = true;
						break;
					} else if (ch == ' ' || ch == '\t') {
						// ignore
					} else {
						pushBack();
						break;
					}
					ch = readChar(zin);
				}
				return s;
			} else if (ch == '\n' || ch < 0 || ch == '\r') {
				endOfLine = true;
				return null;
			} else if (ch == fieldSeparatorRead) {
				// null
				return null;
			} else if (ch <= ' ') {
				// ignore spaces
				continue;
			} else if (ch == commentLineStart) {
				// comment until end of line
				inputBufferStart = -1;
				while (true) {
					ch = readChar(zin);
					if (ch == '\n' || ch < 0 || ch == '\r') {
						break;
					}
				}
				endOfLine = true;
				return null;
			} else {
				// un-delimited value
				while (true) {
					ch = readChar(zin);
					if (ch == fieldSeparatorRead) {
						break;
					} else if (ch == '\n' || ch < 0 || ch == '\r') {
						endOfLine = true;
						break;
					}
				}
				String s = new String(inputBuffer, inputBufferStart,
						inputBufferPos - inputBufferStart - 1);
				inputBufferStart = -1;
				// check un-delimited value for nullString
				return readNull(s.trim());
			}
		}
	}

	private void pushBack() {
		inputBufferPos--;
	}

	private int readChar(InputStream zin) throws IOException {
		if (inputBufferPos >= inputBufferEnd) {
			return readBuffer(zin);
		}
		return inputBuffer[inputBufferPos++];
	}

	private void reset() {
		endOfFile = false;
	}

	private int readBuffer(InputStream zin) throws IOException {
		if (endOfFile) {
			return -1;
		}
		int keep;
		if (inputBufferStart >= 0) {
			keep = inputBufferPos - inputBufferStart;
			if (keep > 0) {
				byte[] src = inputBuffer;
				if (keep + Constants.IO_BUFFER_SIZE > src.length) {
					inputBuffer = new byte[src.length * 2];
				}
				System.arraycopy(src, inputBufferStart, inputBuffer, 0, keep);
			}
			inputBufferStart = 0;
		} else {
			keep = 0;
		}
		inputBufferPos = keep;
		int len = zin.read(inputBuffer, keep, Constants.IO_BUFFER_SIZE);
		if (len == -1) {
			// ensure bufferPos > bufferEnd
			// even after pushBack
			inputBufferEnd = -1024;
			endOfFile = true;
			// ensure the right number of characters are read
			// in case the input buffer is still used
			inputBufferPos++;
			return -1;
		}
		inputBufferEnd = keep + len;
		return inputBuffer[inputBufferPos++];
	}

	private String readNull(String s) {
		return s.equals(nullString) ? null : s;
	}

	private String unEscape(String s) {
		StringBuilder buff = new StringBuilder(s.length());
		int start = 0;
		char[] chars = null;
		while (true) {
			int idx = s.indexOf(escapeCharacter, start);
			if (idx < 0) {
				idx = s.indexOf(fieldDelimiter, start);
				if (idx < 0) {
					break;
				}
			}
			if (chars == null) {
				chars = s.toCharArray();
			}
			buff.append(chars, start, idx - start);
			if (idx == s.length() - 1) {
				start = s.length();
				break;
			}
			buff.append(chars[idx + 1]);
			start = idx + 2;
		}
		buff.append(s.substring(start));
		return buff.toString();
	}

	private boolean isSimpleColumnName(String columnName) {
		for (int i = 0; i < columnName.length(); i++) {
			char ch = columnName.charAt(i);
			if (i == 0) {
				if (ch != '_' && !Character.isLetter(ch)) {
					return false;
				}
			} else {
				if (ch != '_' && !Character.isLetterOrDigit(ch)) {
					return false;
				}
			}
		}
		if (columnName.length() == 0) {
			return false;
		}
		return true;
	}

	private Object[] readRow(InputStream zin) throws SQLException {
		String[] row = new String[columnNames.length];
		try {
			int i = 0;
			while (true) {
				String v = readValue(zin);
				if (v == null) {
					if (endOfLine) {
						if (i == 0) {
							if (endOfFile) {
								return null;
							}
							// empty line
							continue;
						}
						break;
					}
				}
				if (i < row.length) {
					row[i++] = v;
				}
				if (endOfLine) {
					break;
				}
			}
		} catch (IOException e) {
			throw convertException("IOException reading!", e);
		}
		return row;
	}

	private SQLException convertException(String message, Exception e) {
		SQLException s = new SQLException(message, "CSV");
		// ## Java 1.4 begin ##
		s.initCause(e);
		// ## Java 1.4 end ##
		return s;
	}

	/**
	 * 
	 * @param classname
	 *            class name
	 * @param zin
	 *            ZipInputStream
	 * @param filename
	 *            entry name
	 * @return an Object
	 * 
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	public Object xmlReader(Class classname, ZipInputStream zin, String filename)
			throws IOException {
		JAXBContext jc;
		Unmarshaller u;
		Object obj = null;
		try {
			jc = JAXBContext.newInstance(classname);
			u = jc.createUnmarshaller();

			reset();
			ZipEntry entry = null;
			while ((entry = zin.getNextEntry()) != null) {
				if (filename.equalsIgnoreCase(entry.getName())) {
					obj = u.unmarshal(zin);
					return obj;
				}
			}

		} catch (javax.xml.bind.UnmarshalException e) {
			System.out.println("Main: " + e);
		} catch (JAXBException e1) {
			e1.printStackTrace();
		}
		return obj;
	}

	/**
	 * 
	 * @param obj
	 *            the object to save to XML
	 * @param zout
	 *            ZipOutputStream
	 * @param filename
	 *            entry name
	 * 
	 * @throws IOException
	 */
	public void xmlWriter(Object obj, ZipOutputStream zout, String filename)
			throws IOException {
		JAXBContext jc;
		try {
			jc = JAXBContext.newInstance(obj.getClass());
			Marshaller m = jc.createMarshaller();
			m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

			ZipEntry entry = new ZipEntry(filename);
			zout.putNextEntry(entry);
			m.marshal(obj, zout);
			zout.closeEntry();
		} catch (JAXBException e) {
			e.printStackTrace();
		}
	}
/*
	public static void main(String[] args) throws SQLException, IOException {
		Zipper zipper = getInstance();
		ZipOutputStream zout = zipper.makeZipFile("test.zip");
		SimpleResultSet rs = zipper.getSimpleResultSet();
		zipper.csvWriter(zout, rs, "test.csv");
		rs = zipper.getSimpleResultSet();
		zipper.csvWriter(zout, rs, "test101.csv");
		ProjectDescription pd = new ProjectDescription("test",
				"This is a test!");
		zipper.xmlWriter(pd, zout, "project.xml");
		zout.close();

		ZipInputStream zin = zipper.getZipFile("test.zip");
		ResultSet csv = zipper.csvReader(zin, "test.csv");
		zin.close();
		zin = zipper.getZipFile("test.zip");
		csv = zipper.csvReader(zin, "test101.csv");
		zin.close();
		zipper.printContents(csv);
		zin = zipper.getZipFile("test.zip");
		pd = (ProjectDescription) zipper.xmlReader(ProjectDescription.class,
				zin, "project.xml");
		System.out.println();
	}

	private SimpleResultSet getSimpleResultSet() throws SQLException {
		SimpleResultSet rs = new SimpleResultSet();
		rs.addColumn("NAME", Types.VARCHAR, 255, 0);
		rs.addColumn("EMAIL", Types.VARCHAR, 255, 0);
		rs.addRow(new Object[] { "Bob Meier", "bob.meier@abcde.abc" });
		rs.addRow(new Object[] { "John Jones", "john.jones@abcde.abc" });
		rs.addRow(new Object[] { "Sam Diego", "sam.diego@abcde.abc" });
		return rs;
	}

	private void printContents(ResultSet rs) throws SQLException {
		ResultSetMetaData meta = rs.getMetaData();
		while (rs.next()) {
			for (int i = 0; i < meta.getColumnCount(); i++) {
				System.out.println(meta.getColumnLabel(i + 1) + ": "
						+ rs.getString(i + 1));
			}
			System.out.println();
		}
		rs.close();
	}
*/
}
